# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import imp
import logging
import os
import stat
import subprocess
import sys

import gvn.hooks.info


# XXX(epg): I'm not sure about this; 
# If we do have any local logging, it should be somehow configurable,
# defaulting to something in /var/log (or maybe just log to
# repo_dir/hooks/log, and let admins symlink that where they want, or
# use a named pipe.
DEFAULT_LOG_FILE = '/var/tmp/svn.log'

_logger = None
def SetupLogging(logger_name, stream=None, logfile=None, level=logging.INFO):
  """Configure the global logging features.

  """
  global _logger
  if _logger is not None:
    return

  _logger = logging.getLogger(logger_name)
  _logger.setLevel(level)

  if stream is not None:
    stderr_handler = logging.StreamHandler(stream)
    stderr_handler.setFormatter(logging.Formatter('%(message)s'))
    _logger.addHandler(stderr_handler)

  if logfile is not None:
    file_format = logging.Formatter(
        '%(asctime)s %(name)s %(levelname)-8s %(message)s',
        '%a %d %b %Y %H:%M:%S'
        )
    file_handler = logging.FileHandler(logfile, 'a')
    file_handler.setFormatter(file_format)
    _logger.addHandler(file_handler)

def DEBUG(message):
  if _logger is not None:
    _logger.debug(message)

def INFO(message):
  if _logger is not None:
    _logger.info(message)

def WARN(message):
  if _logger is not None:
    _logger.warning(message)


class Hook(object):
  """An interface to call multiple types of hooks (handled by
  passed-in implementations).
  """
  def __init__(self, script, hook_info, impl=None):
    self._script = script
    self._hook_info = hook_info
    self._impl = impl

  script = property(lambda s: s._script,)
  fullpath = property(lambda s: os.path.join(s._hook_info.hook_dir, s.script),)

  def __call__(self):
    logger_name = self._hook_info.logger_name(self.script)
    logger = logging.getLogger(logger_name)
    return self._impl(self.script, self._hook_info, logger)


# XXX(epg): I still don't like this; if some check boils down to simply
# running some other process, then provide a check that does that.  No
# need for these multiple implementations.
def HookExecuteImpl(script, hook_info, logger):
  """Execute it and grab the return code.
  """
  script_path = os.path.join(hook_info.hook_dir, script)
  args = hook_info.arguments
  args.insert(0, script_path)
  return subprocess.call(args)


def HookImportImpl(script, hook_info, logger):
  """The script is a non-executable python file (ends with '.py')
  and therefore the way to evalute it is to 'import' it and call
  RunHook(hook_info, logger).
  """
  rcode = 0
  name = script[:-3]  # strip off the '.py'
  (fp, path, desc) = imp.find_module(name, [hook_info.hook_dir])
  try:
    module = imp.load_module(name, fp, path, desc)
  except:
    WARN("Error importing RunHook for %s/%s" % (hook_info.hook_dir, script))
    raise

  try:
    runhook = module.RunHook
  except AttributeError:
    WARN("Error getting/running RunHook for %s/%s (module %s)" % 
					(hook_info.hook_dir, script, module))
    raise
  return runhook(hook_info, logger)


def GetHook(script, hook_info):
  """Hook factory.
  """
  script_path = os.path.join(hook_info.hook_dir, script)
  mode = os.stat(script_path).st_mode

  # Our hooks directory may have an init, don't try to get Runhook from them
  if script == "__init__.py":
    return None

  if (stat.S_IEXEC & mode):
    return Hook(script, hook_info, HookExecuteImpl)
  elif script.endswith('.py'):
    return Hook(script, hook_info, HookImportImpl)

  # it's neither executable nor "import"able
  return None

def GetHookList(hook_info):
  script_list = []
  try:
    script_list = os.listdir(hook_info.hook_dir)
  except (TypeError, OSError), e:
    # NOTE: default failure
    #
    # If there is no hook_dir directory then there is nothing for this
    # hook script to do. The proper way to skip this hook step is to
    # not run this script at all (hence: delete the symlink pointing to
    # this wrapper if that's the intent).

    # XXX(epg): You say default failure, but then merely warn.  And only to
    # a local log file, at that.  I think it should return a failure
    # message, denying the commit.

    WARN("\nError reading hook_dir: '%s'\n" % (e,))
    return []

  hook_list = []

  # Sort the list of files, allowing for an ordered set of hook scripts,
  # using '010_check_user_access', '020_check_group_access', ...
  # file naming scheme if necessary.
  for script in sorted(script_list):
    hook = GetHook(script, hook_info)
    if hook is not None:
      hook_list.append(hook)
  return hook_list


def main(argv):
  if len(argv) >= 2 and argv[1] == '--invoked-as':
    argv = argv[2:]

  hook_info = gvn.hooks.info.HookInfo(argv, sys.stdin)

  SetupLogging(hook_info.logger_name(), None, DEFAULT_LOG_FILE)
  output = ["\nRunning '%s' hooks ...\n\n" % (hook_info.hook_name,)]

  rcode = 0
  # FIXME: this stuff needs to be reworked --merlin
  message = '\n... PASSED\n'
  for hook in GetHookList(hook_info):
    output.append("  %s:" % (hook.script,))
    hookret = hook()
    if hookret == 0:
      output.append(' PASS\n')
    else:
      output.append(' FAIL\n')
      rcode += 1
      if isinstance(hookret, basestring):
        message = "\n... FAILED: %s\n" % hookret
      else:
        message = "\n... FAILED\n"
      if hook_info.BreakOnHookFailure():
        break

  output.append(message)
  output = ''.join(output)
  INFO(output)
  print >>sys.stderr, output
  logging.shutdown()
  return hook_info.PostProcessHookReturnCode(rcode)
